1   // Licensed under the Apache License, Version 2.0 (the "License");
2   // you may not use this file except in compliance with the License.
3   // You may obtain a copy of the License at
4   //
5   // http://www.apache.org/licenses/LICENSE-2.0
6   //
7   // Unless required by applicable law or agreed to in writing, software
8   // distributed under the License is distributed on an "AS IS" BASIS,
9   // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10  // See the License for the specific language governing permissions and
11  // limitations under the License.
12  
13  package org.apache.tapestry5.internal.services.javascript;
14  
15  import org.apache.tapestry5.SymbolConstants;
16  import org.apache.tapestry5.internal.services.AssetDispatcher;
17  import org.apache.tapestry5.internal.services.RequestConstants;
18  import org.apache.tapestry5.internal.services.ResourceStreamer;
19  import org.apache.tapestry5.ioc.IOOperation;
20  import org.apache.tapestry5.ioc.OperationTracker;
21  import org.apache.tapestry5.ioc.Resource;
22  import org.apache.tapestry5.ioc.annotations.Symbol;
23  import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
24  import org.apache.tapestry5.services.Dispatcher;
25  import org.apache.tapestry5.services.LocalizationSetter;
26  import org.apache.tapestry5.services.PathConstructor;
27  import org.apache.tapestry5.services.Request;
28  import org.apache.tapestry5.services.Response;
29  import org.apache.tapestry5.services.javascript.JavaScriptStackSource;
30  import org.apache.tapestry5.services.javascript.ModuleManager;
31  
32  import javax.servlet.http.HttpServletResponse;
33  import java.io.IOException;
34  import java.util.EnumSet;
35  import java.util.List;
36  import java.util.Locale;
37  import java.util.Map;
38  import java.util.Set;
39  
40  /**
41   * Handler contributed to {@link AssetDispatcher} with key "modules". It interprets the extra path as a module name,
42   * and searches for the corresponding JavaScript module.  Unlike normal assets, modules do not include any kind of checksum
43   * in the URL, and do not set a far-future expires header.
44   *
45   * @see ModuleManager
46   */
47  public class ModuleDispatcher implements Dispatcher
48  {
49      private final ModuleManager moduleManager;
50  
51      private final ResourceStreamer streamer;
52  
53      private final OperationTracker tracker;
54  
55      private final JavaScriptStackSource javaScriptStackSource;
56  
57      private final JavaScriptStackPathConstructor javaScriptStackPathConstructor;
58  
59      private final LocalizationSetter localizationSetter;
60  
61      private final String requestPrefix;
62  
63      private final String stackPathPrefix;
64  
65      private final boolean compress;
66  
67      private final Set<ResourceStreamer.Options> omitExpiration = EnumSet.of(ResourceStreamer.Options.OMIT_EXPIRATION);
68  
69      private Map<String, String> moduleNameToStackName;
70  
71      public ModuleDispatcher(ModuleManager moduleManager,
72                              ResourceStreamer streamer,
73                              OperationTracker tracker,
74                              PathConstructor pathConstructor,
75                              JavaScriptStackSource javaScriptStackSource,
76                              JavaScriptStackPathConstructor javaScriptStackPathConstructor,
77                              LocalizationSetter localizationSetter,
78                              String prefix,
79                              @Symbol(SymbolConstants.ASSET_PATH_PREFIX)
80                              String assetPrefix,
81                              boolean compress)
82      {
83          this.moduleManager = moduleManager;
84          this.streamer = streamer;
85          this.tracker = tracker;
86          this.javaScriptStackSource = javaScriptStackSource;
87          this.javaScriptStackPathConstructor = javaScriptStackPathConstructor;
88          this.localizationSetter = localizationSetter;
89          this.compress = compress;
90  
91          requestPrefix = pathConstructor.constructDispatchPath(compress ? prefix + ".gz" : prefix) + "/";
92          stackPathPrefix = pathConstructor.constructDispatchPath(assetPrefix, RequestConstants.STACK_FOLDER) + "/";
93      }
94  
95      public boolean dispatch(Request request, Response response) throws IOException
96      {
97          String path = request.getPath();
98  
99          if (path.startsWith(requestPrefix))
100         {
101             String extraPath = path.substring(requestPrefix.length());
102 
103             Locale locale = request.getLocale();
104 
105             if (!handleModuleRequest(locale, extraPath, response))
106             {
107                 response.sendError(HttpServletResponse.SC_NOT_FOUND, String.format("No module for path '%s'.", extraPath));
108             }
109 
110             return true;
111         }
112 
113         return false;
114 
115     }
116 
117     private boolean handleModuleRequest(Locale locale, String extraPath, Response response) throws IOException
118     {
119         // Ensure request ends with '.js'.  That's the extension tacked on by RequireJS because it expects there
120         // to be a hierarchy of static JavaScript files here. In reality, we may be cross-compiling CoffeeScript to
121         // JavaScript, or generating modules on-the-fly, or exposing arbitrary Resources from somewhere on the classpath
122         // as a module.
123 
124         int dotx = extraPath.lastIndexOf('.');
125 
126         if (dotx < 0)
127         {
128             return false;
129         }
130 
131         if (!extraPath.substring(dotx + 1).equals("js"))
132         {
133             return false;
134         }
135 
136         final String moduleName = extraPath.substring(0, dotx);
137 
138         String stackName = findStackForModule(moduleName);
139 
140         if (stackName != null)
141         {
142             localizationSetter.setNonPersistentLocaleFromLocaleName(locale.toString());
143             List<String> libraryUrls = javaScriptStackPathConstructor.constructPathsForJavaScriptStack(stackName);
144             if (libraryUrls.size() == 1)
145             {
146                 String firstUrl = libraryUrls.get(0);
147                 if (firstUrl.startsWith(stackPathPrefix))
148                 {
149                     response.sendRedirect(firstUrl);
150                     return true;
151                 }
152             }
153         }
154 
155         return tracker.perform(String.format("Streaming %s %s",
156                 compress ? "compressed module" : "module",
157                 moduleName), new IOOperation<Boolean>()
158         {
159             public Boolean perform() throws IOException
160             {
161                 Resource resource = moduleManager.findResourceForModule(moduleName);
162 
163                 if (resource != null)
164                 {
165                     // Slightly hacky way of informing the streamer whether to supply the
166                     // compressed or default stream. May need to iterate the API on this a bit.
167                     return streamer.streamResource(resource, compress ? "z" : "", omitExpiration);
168                 }
169 
170                 return false;
171             }
172         });
173     }
174 
175     private String findStackForModule(String moduleName)
176     {
177         return getModuleNameToStackName().get(moduleName);
178     }
179 
180     private Map<String, String> getModuleNameToStackName()
181     {
182 
183         if (moduleNameToStackName == null)
184         {
185             moduleNameToStackName = CollectionFactory.newMap();
186 
187             for (String stackName : javaScriptStackSource.getStackNames())
188             {
189                 for (String moduleName : javaScriptStackSource.getStack(stackName).getModules())
190                 {
191                     moduleNameToStackName.put(moduleName, stackName);
192                 }
193             }
194         }
195 
196         return moduleNameToStackName;
197     }
198 }